package com.mapforge.gui.frames.tileset;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.mapforge.Constants;
import com.mapforge.filechooser.MapEditorFileChooser;
import com.mapforge.gui.map.TilesetPanel;
import com.mapforge.listener.TilesetEditEvent;
import com.mapforge.listener.TilesetEditListener;
import com.mapforge.map.Tileset;
import com.mapforge.map.tools.undo.CompositeUndoRedo;
import com.mapforge.map.tools.undo.UndoRedo;
import com.mapforge.map.tools.undo.tilesetedit.AbstractTilesetEdit;
import com.mapforge.map.tools.undo.tilesetedit.TileDimensionAndImageEdit;
import com.mapforge.map.tools.undo.tilesetedit.TilesetDimensionEdit;
import com.mapforge.map.tools.undo.tilesetedit.TilesetEmptyIndexEdit;
import com.mapforge.map.tools.undo.tilesetedit.TilesetImageEdit;
import com.mapforge.map.tools.undo.tilesetedit.TilesetNameEdit;
import com.mapforge.util.ImageUtil;

/**
 * All panels displayed in the tileset editor dialog interested in manipulating
 * tileset values should extend this class. Classes in the tileset editor not
 * extending this class will not be informed by the editor when the current
 * tileset changes.
 * 
 * Extending classes provide custom behavior by overriding methods
 * {@link #setTileset(Tileset)} and {@link #applyTilesetChanges()}
 * 
 * @author William Morrison
 * @see TilesetEditorDialog
 */
public class TilesetPropertiesPanel extends JPanel {
	/**
	 * Short description of this panel's purpose
	 */
	private String description;
	/**
	 * Default serial version ID, here to remove warnings
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * Listeners interested in tileset edits.
	 */
	private List<TilesetEditListener> editListeners;

	/**
	 * Key listener for text field components (image path, tileset name, etc.)
	 */
	private KeyListener keyListener;

	/**
	 * Change listener for spinners
	 */
	private ChangeListener changeListener;

	/**
	 * Action listener for buttons in this dialog
	 */
	private ActionListener actionListener;

	/**
	 * Tileset this panel is displaying information about.
	 */
	private Tileset tileset;

	/**
	 * Component to display the tileset
	 */
	private TilesetPanel tilesetPanel;
	private JScrollPane tilesetScrollPane;

	private JButton btnBrowse;

	private JTextField txtTilesetName;
	private JTextField txtTilesetImgPath;

	private SpinnerNumberModel tileWidthModel;
	private SpinnerNumberModel tileHeightModel;
	private SpinnerNumberModel emptyTileIndexModel;

	private JSpinner spnTileWidth;
	private JSpinner spnTileHeight;
	private JSpinner spnEmptyTileIndex;

	private static final FlowLayout LEFT_FLOW = new FlowLayout(FlowLayout.LEFT,
			2, 2);

	/**
	 * Constructs a new tileset panel with a null tileset.
	 */
	public TilesetPropertiesPanel() {
		this(null, "Missing Description");
	}

	/**
	 * Constructs a new tileset panel with the specified tileset.
	 * 
	 * @param tileset
	 *            specified tileset to be edited by this panel
	 */
	public TilesetPropertiesPanel(Tileset tileset, String description) {
		this.tileset = tileset;
		editListeners = new ArrayList<TilesetEditListener>();
		editListeners = new ArrayList<TilesetEditListener>();

		// init listeners
		actionListener = new PropertiesActionListener();
		changeListener = new DefaultChangeListener();
		keyListener = new DefaultKeyListener();

		// init GUI components
		txtTilesetName = new JTextField(20);
		txtTilesetImgPath = new JTextField(20);
		txtTilesetName.addKeyListener(keyListener);
		txtTilesetImgPath.addKeyListener(keyListener);

		tileWidthModel = new SpinnerNumberModel(
				Constants.DEFAULT_TILE_DIMENSION.width, 1, 512, 1);
		tileHeightModel = new SpinnerNumberModel(
				Constants.DEFAULT_TILE_DIMENSION.height, 1, 512, 1);
		emptyTileIndexModel = new SpinnerNumberModel(0, 0, 0, 1);
		btnBrowse = new JButton("Browse...");
		btnBrowse.addActionListener(actionListener);

		// layout GUI
		initialize();

		// set values in GUI to match tileset if tileset was specified
		if (tileset != null) {
			this.tileset = tileset;
			emptyTileIndexModel.setMaximum(tileset.getImages().length);
			txtTilesetName.setText(tileset.getName());
			tileWidthModel.setValue(tileset.getTileWidth());
			tileHeightModel.setValue(tileset.getTileHeight());
			emptyTileIndexModel.setValue(tileset.getEmptyTileIndex());
		} else {
			spnTileWidth.setEnabled(false);
			spnTileHeight.setEnabled(false);
			spnEmptyTileIndex.setEnabled(false);
		}
	}

	public void reset() {
		spnTileWidth.setEnabled(false);
		spnTileHeight.setEnabled(false);
		spnEmptyTileIndex.setEnabled(false);
		spnTileWidth.setValue(1);
		spnTileHeight.setValue(1);
		spnEmptyTileIndex.setValue(0);
		tilesetPanel.setTileset(null, null);
	}

	protected void initialize() {
		Box box = Box.createVerticalBox();
		JPanel namePanel = new JPanel(LEFT_FLOW);
		JPanel imgPathPanel = new JPanel(LEFT_FLOW);
		tilesetPanel = new TilesetPanel();
		Dimension tilesetPanelDim = new Dimension(300, 300);
		tilesetPanel.setPreferredSize(tilesetPanelDim);
		tilesetScrollPane = new JScrollPane(tilesetPanel);
		tilesetScrollPane.setPreferredSize(tilesetPanelDim);

		namePanel.add(new JLabel("Tileset Name:"));
		namePanel.add(txtTilesetName);

		imgPathPanel.add(new JLabel("Tileset Image:"));
		imgPathPanel.add(txtTilesetImgPath);
		imgPathPanel.add(btnBrowse);

		box.add(imgPathPanel);
		box.add(tilesetScrollPane);
		box.add(namePanel);

		spnTileWidth = new JSpinner(tileWidthModel);
		spnTileHeight = new JSpinner(tileHeightModel);
		spnEmptyTileIndex = new JSpinner(emptyTileIndexModel);
		spnEmptyTileIndex.setPreferredSize(spnTileWidth.getPreferredSize());
		box.add(createPanel("Tile Width:", spnTileWidth));
		box.add(createPanel("Tile Height:", spnTileHeight));
		box.add(createPanel("Empty Index:", spnEmptyTileIndex));

		add(box, BorderLayout.CENTER);
	}

	private JPanel createPanel(String str, JSpinner spinner) {
		JLabel label = new JLabel(str);
		JPanel p = new JPanel(LEFT_FLOW);
		p.add(label);
		spinner.addChangeListener(changeListener);
		p.add(spinner);
		return p;
	}

	public void setTileset(Tileset tileset) {
		this.tileset = tileset;
		displayTilesetValues();
	}

	private void displayTilesetValues() {
		// set values in GUI to match tileset if tileset was specified
		if (tileset != null) {
			emptyTileIndexModel.setMaximum(tileset.getTiles().length);
			txtTilesetName.setText(tileset.getName());
			if (tileset.getImagePath() != null
					&& !tileset.getImagePath().isEmpty())
				this.txtTilesetImgPath.setText(tileset.getImagePath());
			else
				txtTilesetImgPath.setText("No image specified");
			tileWidthModel.setValue(tileset.getTileWidth());
			tileHeightModel.setValue(tileset.getTileHeight());
			emptyTileIndexModel.setValue(tileset.getEmptyTileIndex());
		}
	}

	public void editCurrentTileset() {
		if (tileset != null)
			displayTilesetValues();
		setVisible(true);
	}

	public void applyTilesetChanges() {
		int tileW = tileWidthModel.getNumber().intValue();
		int tileH = tileHeightModel.getNumber().intValue();
		String imagePath = txtTilesetImgPath.getText();

		// whether changes in properties occurred
		boolean imgChange = !tileset.getImagePath().equals(
				txtTilesetImgPath.getText());
		boolean tileDimChange = tileset.getTileWidth() != tileW
				|| tileset.getTileHeight() != tileH;
		boolean nameChange = txtTilesetName.getText().equals(tileset.getName());

		// need to reload image
		if (imgChange || tileDimChange) {
			File imageFile = new File(imagePath);
			// attempt to load image
			if (imageFile.exists()) {
				try {
					BufferedImage img = ImageUtil.getImage(imageFile.toURI()
							.toURL());
					tileset.setImages(img, tileW, tileH);
				} catch (IOException e2) {
					JOptionPane
							.showMessageDialog(this,
									"Unable to load and set tileset tiles using specified image.");
					return;
				}
			}
			// ERROR: couldn't find image file.
			else {
				if (imagePath.length() > 20) {
					imagePath = imagePath.substring(0, 17) + "...";
				}
				JOptionPane.showMessageDialog(this,
						"Couldn't locate image file \"" + imagePath + "\"");
				return;
			}
		}

		// add all edits to the edit list if they've occured
		List<AbstractTilesetEdit> edits = new ArrayList<AbstractTilesetEdit>();
		if (nameChange) {
			edits.add(new TilesetNameEdit(tileset, tileset.getName()));
			tileset.setName(this.txtTilesetName.getText());
		}
		if (tileset.getEmptyTileIndex() != emptyTileIndexModel.getNumber()
				.intValue()) {
			edits.add(new TilesetEmptyIndexEdit(tileset, tileset
					.getEmptyTileIndex()));
			tileset.setEmptyTileIndex(emptyTileIndexModel.getNumber()
					.intValue());
		}
		if (tileDimChange && !imgChange) {
			edits.add(new TilesetDimensionEdit(tileset, tileset.getTileWidth(),
					tileset.getTileHeight()));
			tileset.setTileDimensions(tileW, tileH);
		} else if (imgChange && !tileDimChange) {
			edits.add(new TilesetImageEdit(tileset, tileset.getImagePath()));
			tileset.setImagePath(imagePath);
		} else if (imgChange && tileDimChange) {
			edits.add(new TileDimensionAndImageEdit(tileset, tileset
					.getImagePath(), tileset.getTileWidth(), tileset
					.getTileHeight()));
			tileset.setImagePath(imagePath);
			tileset.setTileDimensions(tileW, tileH);
		}
		// fire only a single edit if a single property was edited
		if (edits.size() == 1) {
			fireUndoableEditPerformed(edits.get(0));
		}
		// fire a compound edit if more than one property was edited
		else {
			CompositeUndoRedo compoundEdit = new CompositeUndoRedo(null,
					"Tileset Properties");
			for (int i = 0; i < edits.size(); i++)
				compoundEdit.addEdit(edits.get(i));
			fireUndoableEditPerformed(compoundEdit);
		}
	}

	public void addTilesetEditListener(TilesetEditListener listener) {
		editListeners.add(listener);
	}

	public boolean removeTilesetEditListener(TilesetEditListener listener) {
		return editListeners.remove(listener);
	}

	public void clearTilesetEditListeners() {
		editListeners.clear();
	}

	/**
	 * Returns the tileset this panel allows the user to manipulate.
	 * 
	 * @return tileset this panel allows the user to manipulate
	 */
	public Tileset getTileset() {
		return tileset;
	}

	/**
	 * Enables models and sets their maximum values, then copies the tileset
	 * values to the corresponding GUI components for display.
	 */
	private void updateGUIFields() {
		if (this.tileset != null) {
			spnTileWidth.setEnabled(true);
			spnTileHeight.setEnabled(true);
			spnEmptyTileIndex.setEnabled(true);
			int tilew = tileset.getTileWidth();
			int tileh = tileset.getTileHeight();
			int tw = tileWidthModel.getNumber().intValue();
			int th = tileHeightModel.getNumber().intValue();
			int empty = emptyTileIndexModel.getNumber().intValue();

			tileWidthModel.setMaximum(tilew);
			tileHeightModel.setMaximum(tileh);
			tileWidthModel.setMinimum(1);
			tileHeightModel.setMinimum(1);

			int totalTiles = (tileh / tileHeightModel.getNumber().intValue())
					* (tilew / tileWidthModel.getNumber().intValue());
			emptyTileIndexModel.setMaximum(totalTiles - 1);

			tileWidthModel.setValue(tw);
			tileHeightModel.setValue(th);
			emptyTileIndexModel.setValue(empty);
			displayTilesetValues();
		}
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public String getDescription() {
		return description;
	}

	/**
	 * When the user performs an action (Keystroke, click a button, change a
	 * field in a list etc.) this change listener should be informed of the
	 * occurance to update the frame this panel resides in.
	 * 
	 * @param listener
	 */
	public void registerTilesetChangeListener(ChangeListener listener) {
		this.changeListener = listener;
	}

	protected void fireTilesetChangedEvent(Object source) {
		this.changeListener.stateChanged(new ChangeEvent(source));
	}

	/**
	 * 
	 * @param edit
	 */
	protected void fireUndoableEditPerformed(UndoRedo undoRedo) {
		TilesetEditEvent event = new TilesetEditEvent(this, undoRedo);
		for (int i = 0; i < editListeners.size(); i++) {
			editListeners.get(i).tilesetEditHappened(event);
		}
	}

	private class DefaultKeyListener extends KeyAdapter {
		public void keyPressed(KeyEvent e) {
		}
	}

	private int calculateTotalTiles() {
		int tw = tileset.getTileWidth();
		int th = tileset.getTileHeight();
		return (tw / tileWidthModel.getNumber().intValue())
				* (th / tileHeightModel.getNumber().intValue());
	}

	private class DefaultChangeListener implements ChangeListener {
		public void stateChanged(ChangeEvent e) {
			Object src = e.getSource();
			if (src == spnTileWidth) {
				emptyTileIndexModel.setMaximum(calculateTotalTiles());
			} else if (src == spnTileHeight) {
				emptyTileIndexModel.setMaximum(calculateTotalTiles());
			}
		}
	}

	private class PropertiesActionListener implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			Object obj = e.getSource();
			if (obj == btnBrowse) {
				MapEditorFileChooser chooser = MapEditorFileChooser.getHandle();
				int opt = chooser.showOpenDialog(TilesetPropertiesPanel.this);
				if (opt == JFileChooser.APPROVE_OPTION) {
					File selectedFile = chooser.getSelectedFile();
					if (MapEditorFileChooser.getImageFileFilter().accept(
							selectedFile)) {
						try {
							BufferedImage img = ImageUtil.getImage(selectedFile
									.toURI().toURL());
							tileset.setImages(img, img.getWidth(), img
									.getHeight());
							tilesetPanel.setTileset(tileset, img);
							tilesetPanel.repaint();
							txtTilesetImgPath.setText(selectedFile
									.getAbsolutePath());
							updateGUIFields();
							revalidate();
						} catch (IOException e1) {
							e1.printStackTrace();
							JOptionPane.showMessageDialog(
									TilesetPropertiesPanel.this,
									"Unable to load " + selectedFile.getName());
						}
					} else {
						// TODO check if is valid tileset file
					}
				}
			}
		}
	}
}